;;; packages.el --- ipython Layer packages File for Spacemacs  -*- lexical-binding: nil; -*-
;;
;; Copyright (c) 2012-2025 Sylvain Benner & Contributors
;;
;; Author: Sylvain Benner <sylvain.benner@gmail.com>
;; URL: https://github.com/syl20bnr/spacemacs
;;
;; This file is not part of GNU Emacs.
;;
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.


(setq ipython-notebook-packages '(ein company))

(defun ipython-notebook/init-ein ()
  (use-package ein
    :init
    (spacemacs/set-leader-keys "atil" 'ein:login
      "atir" 'ein:run
      "atis" 'ein:stop)
    (spacemacs/declare-prefix "ati" "ipython notebook")
    :config
    (with-eval-after-load 'ein-notebook
      (add-hook 'ein:notebook-mode-hook
                (lambda ()
                  (add-to-list 'minor-mode-overriding-map-alist
                               `(ein:notebook-mode . ,ein:notebook-mode-map))))
      (mapc
       (lambda (mode)
         (evil-define-minor-mode-key mode 'ein:notebook-mode
           (kbd "C-<return>") 'ein:worksheet-execute-cell-km
           (kbd "S-<return>") 'ein:worksheet-execute-cell-and-goto-next-km))
       '(insert hybrid normal))
      (evil-define-minor-mode-key 'normal 'ein:notebook-mode
        "gj" 'ein:worksheet-goto-next-input-km
        "gk" 'ein:worksheet-goto-prev-input-km)
      (let ((bindings '(("j" ein:worksheet-goto-next-input-km)
                        ("k" ein:worksheet-goto-prev-input-km)
                        ("J" ein:worksheet-move-cell-down-km)
                        ("K" ein:worksheet-move-cell-up-km)
                        ("e" ein:worksheet-toggle-output-km)
                        ("d" ein:worksheet-kill-cell-km)
                        ("y" ein:worksheet-copy-cell-km)
                        ("p" ein:worksheet-yank-cell-km)
                        ("m" ein:worksheet-merge-cell-km)
                        ("s" ein:worksheet-split-cell-at-point-km)
                        ("o" ein:worksheet-insert-cell-below-km)
                        ("O" ein:worksheet-insert-cell-above-km)
                        ("t" ein:worksheet-toggle-cell-type-km)
                        ("RET" ein:worksheet-execute-cell-and-goto-next-km)
                        ("l" ein:worksheet-clear-output-km)
                        ("L" ein:worksheet-clear-all-output-km)
                        ("C-s" ein:notebook-save-notebook-command-km)
                        ("C-r" ein:notebook-rename-command-km)
                        ("x" ein:notebook-close-km)
                        ("z" ein:notebook-kernel-interrupt-command-km))))
        (apply #'spacemacs/set-leader-keys-for-minor-mode 'ein:notebook-mode
               (cl-mapcan
                (lambda (bind)
                  (if (fboundp (cl-second bind))
                      bind
                    (prog1 nil
                      (display-warning
                       'warn (format "ipython-notebook/init-ein: undefined %s"
                                     (cl-second bind))))))
                (copy-tree bindings)))
        (eval (append '(spacemacs|define-transient-state
                           ipython-notebook
                         :title "EIN Transient State"
                         :evil-leader-for-mode (ein:notebook-mode . ".")
                         :bindings
                         ("q" nil :exit t))
                      bindings
                      `(:doc ,(ipython-notebook/transient-doc bindings))))))))

(defun ipython-notebook/post-init-company ()
  (add-hook 'ein:notebook-mode-hook #'spacemacs//ein-setup-company))

(defun ipython-notebook/max-by-prefix (alist)
  (seq-reduce (lambda (lst1 lst2) (if (> (cl-second lst1)
                                         (cl-second lst2))
                                      lst1 lst2))
              (cdr alist) (car alist)))

(defun ipython-notebook/count-by-prefix (alist)
  (mapcar (lambda (lst)
            (cons (car lst) (list (length (cdr lst)))))
          alist))

(defun ipython-notebook/commands-by-prefix-alist (commands)
  "Return ((P1 P1-CMD1 P1-CMD2) (P2 P2-CMD1 P2-CMD2) ... )"
  (let* ((commands (if (symbolp (car commands))
                       (mapcar #'symbol-name commands)
                     commands))
         (upto (ipython-notebook/prefix commands))
         result)
    (cl-flet ((get-prefix
               (command)
               (intern (mapconcat #'identity
                                  (cl-subseq (split-string command (regexp-quote "-"))
                                          0 (1+ upto))
                                  "-"))))
      (mapc (lambda (command)
              (let ((lst (alist-get (get-prefix command) result)))
                (setf (alist-get (get-prefix command) result)
                      (cons (intern command) lst))))
            commands)
      result)))

(cl-defun ipython-notebook/prefix (commands &key (separator "-") (suffix-p nil))
  "Return index of first different prefix among COMMANDS if each were split on SEPARATOR.
For example, return 2 if COMMANDS are '(ein:notebook-foo ein:notebook-foo-bar)."
  (let* ((commands (if (symbolp (car commands))
                       (mapcar #'symbol-name commands)
                     commands))
         (split-up (mapcar (lambda (command)
                             (funcall (if suffix-p #'reverse #'identity)
                                      (split-string command (regexp-quote separator))))
                           commands)))
    (cl-loop for result from 0
             for bogey = (nth result (car split-up))
             if (or (null bogey)
                    (null (cdr split-up))
                    (cl-some (lambda (lst) (not (string= bogey (nth result lst))))
                             (cdr split-up)))
             return (funcall (if suffix-p #'- #'identity) result)
             end)))

(defun ipython-notebook/transient-doc (bindings)
  (let* ((commands-by (ipython-notebook/commands-by-prefix-alist (mapcar #'cl-second bindings)))
         (counts-by (ipython-notebook/count-by-prefix commands-by))
         (max-by (ipython-notebook/max-by-prefix counts-by))
         (main (cl-first max-by))
         (n-main (cl-second max-by))
         (n-other (apply #'+ (mapcar (lambda (lst) (if (eq (cl-first lst) main)
                                                       0 (cl-second lst)))
                                     counts-by)))
         (max-col 3)
         (spread (min max-col (ceiling (/ n-main (* 1.3 n-other)))))
         (main-commands (cdr (assq main commands-by)))
         (other-commands (cl-mapcan #'cdr (cl-remove-if (lambda (lst)
                                                       (eq (car lst) main))
                                                     commands-by)))
         (other-from (ipython-notebook/prefix other-commands))
         (other-to (ipython-notebook/prefix other-commands :suffix-p t))
         (main-from (ipython-notebook/prefix main-commands))
         (main-to (ipython-notebook/prefix main-commands :suffix-p t)))
    (cl-flet ((get-key (command) (car (rassoc (list command) bindings)))
              (massage (command from to)
                       (let ((toks (split-string (symbol-name command) (regexp-quote "-"))))
                         (mapconcat #'identity
                                    (cl-subseq toks from (+ (length toks) to))
                                    "-"))))
      (cl-macrolet ((rescol
                     (result commands from to)
                     `(let* ((key-width 10)
                             (col-width 20)
                             (format-str (format "%%-%ds%%-%ds" key-width col-width)))
                        (if-let* ((command (pop ,commands)))
                            (let ((massaged (massage command ,from ,to)))
                              (setq ,result
                                    (concat ,result
                                            (format format-str
                                                    (format "[_%s_]^^" (get-key command))
                                                    (cl-subseq massaged 0
                                                            (min (length massaged) col-width))))))
                          (setq ,result (concat ,result (format format-str "" "")))))))
        (cl-loop
         with result = "\n"
         with betw = (make-string 1 ? )
         with col = 1
         if (= col spread)
         do (rescol result other-commands other-from other-to)
         and do (setq result (concat result "\n"))
         and do (setq col 1)
         else
         do (rescol result main-commands main-from main-to)
         and do (setq result (concat result betw))
         and do (cl-incf col)
         end
         until (and (null other-commands) (null main-commands))
         finally return result)))))
